#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
#include <unistd.h>
#include <string.h>
#include <pthread.h>
#include <arpa/inet.h>

#include "connecting.h"
#include "user_info_handler.h"
#include "utils.h"

#define kCOMMAND_CONNECT "connect"
#define kSEND_REQUEST_INTERVAL_MINUTES 3
#define kDEFAULT_LISTEN_PORT 4399
#define kUSER_COMMAND_MAX_LENGTH 30

typedef enum connection_value {
    kCONNECTION_VALUE_REQUEST,
    kCONNECTION_VALUE_RESPOND
}TCONNECTION_VALUE;

typedef enum user_command_code {
    kUSER_COMMAND_CODE_INVALID,
    kUSER_COMMAND_CODE_CONNECT
}TUSER_COMMAND_CODE;

typedef struct msg {
    uint8_t connection;
}Msg;

typedef struct user_input_info {
    int command_code;
    char *user_input_ip_address_str;
}UserInputInfo;


static pthread_t connect_to_peer_player_thread_id;


static Msg buildupMsg(uint8_t connection_value)
{
    Msg new_msg = {.connection = connection_value};

    return new_msg;
}


static Msg buildupRequestMsg()
{
    return buildupMsg(kCONNECTION_VALUE_REQUEST);
}


static Msg buildupRespondMsg()
{
    return buildupMsg(kCONNECTION_VALUE_RESPOND);
}


static void periodicallySendMsg(int sockfd, Msg sending_msg, struct sockaddr_in dest_address, int interval, char *prompt)
{
    while(1) {
        printf("%s\n", prompt);
        sendto(sockfd, &sending_msg, sizeof(Msg), MSG_CONFIRM, &dest_address, sizeof(dest_address));
        sleep(interval);
    }
}


static void *connectToPeerPlayerThread(void *arg)
{
    UserInfo *info = (UserInfo *)arg;
    Msg request_msg = buildupRequestMsg();
    struct sockaddr_in opponent_address = UserInfoHandler_GetOpponentAddress(info);

    char opponent_address_str[kIP_ADDRESS_STRING_MAX_LENGTH] = {0};
    char prompt_str[64] = {0};

    parseIPv4AddressToString(info->opponent_address.sin_addr, opponent_address_str);
    sprintf(prompt_str, "connecting to %s ...", opponent_address_str);
    periodicallySendMsg(info->sockfd, request_msg, opponent_address, kSEND_REQUEST_INTERVAL_MINUTES, prompt_str);
}


static void getUserInputFromStdin(char *buffer)
{
    fgets(buffer, kUSER_COMMAND_MAX_LENGTH, stdin);
    buffer[kUSER_COMMAND_MAX_LENGTH - 1] = '\0';
}


static UserInputInfo parseUserInput(char *buffer)
{
    char *command;
    UserInputInfo user_input_info;

    getUserInputFromStdin(buffer);
    stripNewlineCharacter(buffer);
    command = strtok(buffer, " ");

    if (strcmp(command, kCOMMAND_CONNECT) == 0) {
        user_input_info.command_code = kUSER_COMMAND_CODE_CONNECT;
        user_input_info.user_input_ip_address_str = strtok(NULL, " ");
    } else {
        user_input_info.command_code = kUSER_COMMAND_CODE_INVALID;
    }

    return user_input_info;
}


static void connectToPeerPlayer(UserInfo *info, char *opponent_ip_address_str)
{
    struct sockaddr_in temp_opponent_address;
 
    bzero(&temp_opponent_address, sizeof(temp_opponent_address));
    temp_opponent_address.sin_port = htons(kDEFAULT_LISTEN_PORT);
    temp_opponent_address.sin_family = AF_INET;

    if (tryParsingIPStringToInternetAddress(opponent_ip_address_str, &temp_opponent_address.sin_addr)
        == kUTILS_TRYING_FUNCTIONS_RETURN_CODE_FAILED) 
    {                   
        printf("IP address \"%s\" is invalid.\n", opponent_ip_address_str);
        return;
    }

    UserInfoHandler_SetPlayerMode(info, kPLAYER_MODE_POSITIVE);

    pthread_cleanup_push(pthread_mutex_unlock, &info->lock);

    UserInfoHandler_SetOpponentAddress(info, temp_opponent_address);
    UserInfoHandler_SetConnectionExpect(info, kCONNECTION_VALUE_RESPOND);

    pthread_cleanup_pop(0);

    pthread_create(&connect_to_peer_player_thread_id, NULL, connectToPeerPlayerThread, info);
}


static void replyConnectionRespond(int sockfd, struct sockaddr_in opponent_address)
{
    Msg reply_msg = buildupRespondMsg();

    sendto(sockfd, &reply_msg, sizeof(reply_msg), MSG_CONFIRM, &opponent_address, sizeof(opponent_address));
}


static void printOpponentIP(struct sockaddr_in opponent_address)
{
    char opponent_address_str[kIP_ADDRESS_STRING_MAX_LENGTH] = {0};

    parseIPv4AddressToString(opponent_address.sin_addr, opponent_address_str);
    printf("Opponent's IP address is %s\n", opponent_address_str);
}


static void bindLocalSocketToDefaultPort(int sockfd)
{
    struct sockaddr_in servaddr = {
        .sin_port = htons(kDEFAULT_LISTEN_PORT),
        .sin_family = AF_INET,
        .sin_addr.s_addr = INADDR_ANY
    };

    if (bind(sockfd, (const struct sockaddr *)&servaddr, sizeof(servaddr)) < 0) {
        perror("bind failed");
        exit(EXIT_FAILURE);
    }
}


static int connectToPeerPlayThreadIsExist()
{
    return connect_to_peer_player_thread_id != 0;
}


static ShakeHandInfo getShakeHandInfoFromUserInfo(UserInfo *user_info)
{
    ShakeHandInfo shake_hand_info;
    shake_hand_info.opponent_address = UserInfoHandler_GetOpponentAddress(user_info);
    shake_hand_info.player_mode = UserInfoHandler_GetPlayerMode(user_info);
    
    return shake_hand_info;
}


static void *userCommandHandleThread(void *arg)
{
    UserInfo *info = (UserInfo *)arg;
    UserInputInfo user_input_info;    
    char command_input_buffer[kUSER_COMMAND_MAX_LENGTH] = {0};

    while(1) {
        user_input_info = parseUserInput(command_input_buffer);

        if (user_input_info.command_code == kUSER_COMMAND_CODE_INVALID) {
            printf("Unknown command.\n");
            continue;
        } else if (user_input_info.command_code == kUSER_COMMAND_CODE_CONNECT) {
            connectToPeerPlayer(info, user_input_info.user_input_ip_address_str);
            break;
        }
    }
}


static void *waitPeerPlayerThread(void *arg)
{
    UserInfo *info = (UserInfo *)arg;
    uint8_t connection_expect;
    Msg recv_msg;
    struct sockaddr_in temp_opponent_address;
    int temp_opponent_address_len;

    bzero(&temp_opponent_address, sizeof(temp_opponent_address));
    temp_opponent_address_len = sizeof(temp_opponent_address);

    bindLocalSocketToDefaultPort(info->sockfd);

    while(1) {
        recvfrom(info->sockfd, &recv_msg, sizeof(recv_msg), MSG_WAITALL, &temp_opponent_address, &temp_opponent_address_len);
        
        connection_expect = UserInfoHandler_GetConnectionExpect(info);
        
        if (recv_msg.connection != connection_expect) {
            continue;
        }

        if (connection_expect == kCONNECTION_VALUE_RESPOND) {
            break;
        }

        if (connection_expect == kCONNECTION_VALUE_REQUEST) {
            UserInfoHandler_SetOpponentAddress(info, temp_opponent_address);
            printOpponentIP(temp_opponent_address);
            replyConnectionRespond(info->sockfd, temp_opponent_address);

            break;
        }
    }
}


ShakeHandInfo Connecting_ShakeHand(int sockfd)
{
    ShakeHandInfo shake_hand_info;
    pthread_t user_command_handle_thread_id;
    pthread_t wait_peer_player_thread_id;

    UserInfo *user_info = UserInfoHandler_NewUserInfo(sockfd, kPLAYER_MODE_PASSIVE, kCONNECTION_VALUE_REQUEST);
    
    pthread_create(&user_command_handle_thread_id, NULL, userCommandHandleThread, user_info);
    pthread_create(&wait_peer_player_thread_id, NULL, waitPeerPlayerThread, user_info);

    pthread_join(wait_peer_player_thread_id, NULL);

    pthread_cancel(user_command_handle_thread_id);

    if (connectToPeerPlayThreadIsExist()) {
        pthread_cancel(connect_to_peer_player_thread_id);
    }
    
    shake_hand_info = getShakeHandInfoFromUserInfo(user_info);

    UserInfoHandler_DestoryUserInfo(user_info);

    return shake_hand_info;
}